Skip to content

Switch Claude execution from services to ephemeral exec sessions#102

Merged
mcintyre94 merged 3 commits intomainfrom
exec-based-claude-execution
Mar 17, 2026
Merged

Switch Claude execution from services to ephemeral exec sessions#102
mcintyre94 merged 3 commits intomainfrom
exec-based-claude-execution

Conversation

@mcintyre94
Copy link
Copy Markdown
Owner

Summary

  • Replaces the service-based Claude execution model (PUT /services/{name}) with ephemeral exec WebSocket sessions. Services restart automatically on sprite wake, causing Claude to re-execute old prompts and burn tokens; exec sessions do not restart.
  • Exec sessions persist on the server for max_run_after_disconnect=3600s, allowing the app to reattach after backgrounding. If the session has expired (sprite slept), falls back to restoring history from Claude's .jsonl session file on the sprite.
  • Splits ExecEvent.data into .stdout / .stderr to prevent heartbeat noise from corrupting the NDJSON parser.
  • Adds one-time migration to clean up leftover wisp-claude-* and wisp-quick-* services on existing sprites (gated on currentServiceName != nil so it's a no-op after first run).

Changes

  • ExecSession — split .data(Data) into .stdout(Data) / .stderr(Data)
  • SpriteChat — add execSessionId: String? for WebSocket reattach
  • SpritesAPIClient — remove service streaming methods; add killExecSession, cleanupLegacyServices
  • ChatViewModel — replace processServiceStream / runReconnectLoop with processExecStream / reattachToExec / restoreFromSessionFile; fix session restore to trigger on both .timedOut and .disconnected results
  • ChatViewModelTests — replace MockServiceLogsProvider with makeExecStream helper; update reconnect tests

Test plan

  • Send a message → exec session created, Claude runs, response streams in
  • Background app mid-response → reopen → reattaches to running exec, scrollback shown without duplication
  • Let sprite sleep after a completed session → reopen → history restored from session file
  • Send new message after restore → Claude resumes with --resume {sessionId}
  • Interrupt mid-response → new message → fresh Claude session (old process killed)
  • First launch on existing install → legacy wisp-claude-* / wisp-quick-* services cleaned up

🤖 Generated with Claude Code

mcintyre94 and others added 2 commits March 16, 2026 19:13
Services restart automatically when a sprite wakes, causing Claude to
re-execute stale prompts and creating runaway duplicate sessions. Exec
sessions don't restart on wake and persist for max_run_after_disconnect
so the app can reattach after backgrounding.

- ExecEvent: split .data into .stdout / .stderr to keep heartbeat noise
  out of the NDJSON parser buffer
- ChatViewModel: replace service streaming with exec WebSocket sessions;
  add processExecStream, reattachToExec, restoreFromSessionFile; update
  reconnectIfNeeded to reattach via execSessionId; interrupt() now kills
  the exec session instead of deleting a service
- SpriteChat: add execSessionId field for reattach across app restarts
- SpritesAPIClient: add killExecSession, cleanupLegacyServices; remove
  streamService, streamServiceLogs, getServiceStatus, deleteService,
  ServiceLogsProvider protocol
- One-time migration: cleanupLegacyServices deletes leftover wisp-claude-*
  and wisp-quick-* services on first launch; becomes a no-op once all
  currentServiceName fields are cleared

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When reattaching after a full app close, an expired exec session causes
a WebSocket connection error, which processExecStream returns as .timedOut
(no data received). The restore condition only checked for .disconnected,
so restoreFromSessionFile was never reached.

Now both .timedOut and .disconnected trigger session file restore when a
sessionId is available. Also clears any error status set by processExecStream
before attempting the restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Code review

1 issue found.

Compilation error: StreamResult does not conform to Equatable

File: WispTests/ChatViewModelTests.swift, in the new test reattachToExec_setsLastSessionCompleteWhenResultReceived

The test uses #expect(result == .completed) where result is of type StreamResult. StreamResult is declared as enum StreamResult: CustomStringConvertible with no Equatable conformance, so the == operator is not synthesized and this will fail to compile. All other tests in this file correctly use pattern matching instead.

Fix Option A: Add Equatable to the StreamResult declaration in ChatViewModel.swift:
enum StreamResult: Equatable, CustomStringConvertible

Fix Option B: Change the assertion to pattern matching (consistent with the rest of the test suite):
guard case .completed = result else {
Issue.record(message)
return
}

Fixes compilation error in ChatViewModelTests where
#expect(result == .completed) requires Equatable conformance.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@mcintyre94 mcintyre94 enabled auto-merge March 17, 2026 19:29
@mcintyre94 mcintyre94 merged commit 57eafc4 into main Mar 17, 2026
2 checks passed
@mcintyre94 mcintyre94 deleted the exec-based-claude-execution branch March 17, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant